package net.kem.android.positioner;

import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

public class PositionerService extends Service {
	public static final String EXTRA_LOCATION = "Extra_Location";
	public static final String EXTRA_LOCATION_ID = "Extra_Location_ID";
	public static final String EXTRA_SCHEDULLED_CALL = "Extra_Schedulled_Call";
	private static final String TEMPLATE_REQUEST = "%d;%s;%d;%s;%1.6f;%1.6f\r\n";
//	private UIHandler uiHandler;
	private LocationManager locationManager;
	private LocationListener locationListener;
	private AlarmManager alarmManager;
	private PendingIntent alarmPI;
	private IBinder binder;
	private BackgroundSender backgroundSender;

	@Override
	public void onCreate() {
		Settings.init(this); // Initialize Setting instance.

//		// Toast within a thread.
//		HandlerThread uiThread = new HandlerThread("UIHandler");
//		uiThread.start();
//		uiHandler = new UIHandler(uiThread.getLooper());

		binder = new LocalBinder();
		// Work with GPS service
		locationListener = new LocationListener() {
			@Override
			public void onStatusChanged(String provider, int status, Bundle extras) {
				String sStatus;
				switch(status) {
					case LocationProvider.AVAILABLE: sStatus = "Available"; break;
					case LocationProvider.OUT_OF_SERVICE: sStatus = "Out-of-service"; break;
					case LocationProvider.TEMPORARILY_UNAVAILABLE: sStatus = "Temporarily unavailable"; break;
					default: sStatus = "Unknown: " + status;
				}
				Log.i("PS GPS status", "Status of provider " + provider + " has changed to " + sStatus);
			}

			@Override
			public void onProviderEnabled(String provider) {
				Log.i("PS GPS status", "Provider " + provider + " enabled");
			}

			@Override
			public void onProviderDisabled(String provider) {
				Log.i("PS GPS status", "Provider " + provider + " disabled");
			}

			@Override
			public void onLocationChanged(Location location) {
				float accuracy = location.getAccuracy();
				Log.d("PS GPS Location Changed", "Accuracy " + accuracy);
				sendLocation(location, 0);
			}
		};

		float gpsRefreshDistance = Settings.getGPSRefreshDistance();
		long gpsRefreshInterval = Settings.getGPSRefreshInterval() * 1000L;
		locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
		addLocationListener(locationListener, gpsRefreshDistance, gpsRefreshInterval);
		// LocationManager.NETWORK_PROVIDER // Can also be used.

		setAuthentication();

		setPeriodicalCalls();

		backgroundSender = new BackgroundSender();
		backgroundSender.start();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if(intent != null && intent.getExtras() != null) {
			boolean isSchedulledCall = intent.getBooleanExtra(EXTRA_SCHEDULLED_CALL, false);
			if (isSchedulledCall) {
				if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
					Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
					sendLocation(location, 0);
				} else {
					Log.d(PositionerService.class.getSimpleName(), "Provider " + LocationManager.GPS_PROVIDER + " is disabled");
				}
			} else {
				Location location = (Location) intent.getExtras().get(EXTRA_LOCATION);
				int locationId = intent.getIntExtra(EXTRA_LOCATION_ID, 0);
				sendLocation(location, locationId);
			}
		}

		// If we get killed, after returning from here, restart
		return START_STICKY;
	}

	@Override
	public void onDestroy() {
		if(alarmManager != null && alarmPI != null) {
			alarmManager.cancel(alarmPI);
		}
		removeLocationListener(locationListener);
		handleUIRequest("onDestroy", "service done");// Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
	}

	void addLocationListener(LocationListener locationListener, float gpsRefreshDistance, long gpsRefreshInterval) {
		if(gpsRefreshInterval == 0L) {
			gpsRefreshInterval = 30000L;//30 seconds
		}
		// GPS service will notify me about new location every gpsRefreshInterval seconds or if I move more than gpsRefreshDistance meters.
		locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, gpsRefreshInterval, gpsRefreshDistance, locationListener);
		Log.d(PositionerService.class.getSimpleName(), "Add Location Listener: " + gpsRefreshInterval + " ms, " + gpsRefreshDistance + " m, " + locationListener.toString());
	}

	void removeLocationListener(LocationListener locationListener) {
		locationManager.removeUpdates(locationListener);
		Log.d(PositionerService.class.getSimpleName(), "Remove Location Listener " + locationListener.toString());
	}

	/**
	 * The IntentService calls this method from the default worker thread with
	 * the intent that started the service. When this method returns,
	 * IntentService stops the service, as appropriate.
	 */
	private void sendLocation(double latitude, double longitude, int locationId) {
		Location location = new Location(LOCATION_SERVICE);
		location.setLatitude(latitude);
		location.setLongitude(longitude);
		sendLocation(location, locationId);
	}

	private void sendLocation(final Location location, final int locationId) {
		if (location == null || (locationId == 0 && isWithinRadius(location, Settings.getPOIRadius()))) {
			return;
		}
		backgroundSender.doIt(locationId, location);
	}

	// ---- Binder BEGIN
	/**
	 * Class used for the client Binder. Because we know this service always
	 * runs in the same process as its clients, we don't need to deal with IPC.
	 */
	class LocalBinder extends Binder {
		String getArressesByLocation(Location location) {
			return PositionerService.this.getArressesByLocation(location);
		}

		void addLocationListener(LocationListener locationListener, float gpsRefreshDistance, long gpsRefreshInterval) {
			if(locationListener == null) {
				locationListener = PositionerService.this.locationListener;
			}
			PositionerService.this.addLocationListener(locationListener, gpsRefreshDistance, gpsRefreshInterval);
		}

		void removeLocationListener(LocationListener locationListener) {
			PositionerService.this.removeLocationListener(locationListener);
		}

		void sendLocation(double latitude, double longitude, int locationId) {
			PositionerService.this.sendLocation(latitude, longitude, locationId);
		}

		void sendLocation(Location location, int locationId) {
			PositionerService.this.sendLocation(location, locationId);
		}

		// Call this service (via PositionerService.LocationPoller) every 'gpsRefreshInterval' milliseconds.
		void setPeriodicalCalls() {
			PositionerService.this.setPeriodicalCalls();
		}
	}

	@Override
	public IBinder onBind(Intent intent) {
		return binder;
	}
	// ---- Binder END

	private static void setAuthentication() {
		final String username = Settings.getUsername();
		final String password = Settings.getPassword();
		Authenticator.setDefault(new Authenticator() {
			@Override
			protected PasswordAuthentication getPasswordAuthentication() {
				return new PasswordAuthentication(username, password.toCharArray());
			}
		});
	}

	// Call this service (via PositionerService.LocationPoller) every 'gpsRefreshInterval' milliseconds.
	private void setPeriodicalCalls() {
		long gpsRefreshInterval = Settings.getGPSRefreshInterval() * 1000L;
		Intent i = new Intent(PositionerService.this, LocationPoller.class).addCategory(Intent.CATEGORY_DEFAULT);
		alarmPI = PendingIntent.getBroadcast(PositionerService.this, 0, i, 0);
		alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
		if(gpsRefreshInterval > 0L) {
			alarmManager.setRepeating(AlarmManager.RTC, SystemClock.currentThreadTimeMillis() + 60000L, gpsRefreshInterval, alarmPI);
		} else {
			alarmManager.cancel(alarmPI);
			Log.i("Alarms", "Positoner service's alarm won't be started since gpsRefreshInterval is 0 ");
		}
	}

	private String getArressesByLocation(Location location) {
		String res = null;
		Geocoder geoCoder = new Geocoder(getBaseContext(), Locale.ENGLISH);// Locale.getDefault()
		try {
			List<Address> addresses = geoCoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
			if (addresses.size() > 0) {
				res = "";
				for (int index = 0; index < addresses.get(0).getMaxAddressLineIndex(); index++)
					res += addresses.get(0).getAddressLine(index) + " ";
			}
		} catch (IOException e) {
			Log.e("convertPointToLocation", "Could not get location address: " + e.getMessage());
		}
		return res;
	}

	private boolean isWithinRadius(Location currentLocation) {
		int radius = Settings.getPOIRadius();
		return isWithinRadius(currentLocation, radius);
	}
	private boolean isWithinRadius(Location currentLocation, int radius) {
		boolean res;
		Map<String, POI> pois = Settings.getPOIs();
		for (POI poi : pois.values()) {
			res = isWithinRadius(currentLocation, poi.getCoordinates(), radius);
			if (res) {
				String poiName = poi.getName();
				Log.d("withinRadius", "Inside " + radius + " meters from " + poiName);
				return true;
			}
		}
		Log.d("withinRadius", "Out of " + radius + " meters from all known POIs");
		return false;
	}
	private boolean isWithinRadius(Location currentLocation, String poiName, int radius) {
		POI poi = Settings.getPOIByName(poiName);
		if (poi != null) {
			if (isWithinRadius(currentLocation, poi.getCoordinates(), radius)) {
				Log.d("withinRadius", "Inside " + radius + " meters from " + poiName);
				return true;
			}
			Log.d("withinRadius", "Out of " + radius + " meters from all known POIs");
		}
		return false;
	}
	private boolean isWithinRadius(Location currentLocation, Location location, int radius) {
		if (location != null) {
			float distance = currentLocation.distanceTo(location);
			if (distance < radius) {
				return true;
			}
		}
		return false;
	}

	private class WithinPOIRadiusSersor extends Thread {
		boolean useShortGPSDelay = false;
		private boolean withinPOIradius = false;
		private boolean goOn = true;
		WithinPOIRadiusSersor() {
			super("Within POI radius timer");
			setDaemon(true);
		}
		void radiusIn() {
			if(!withinPOIradius) {
				withinPOIradius = true;
				synchronized(this) {
					notifyAll();
				}
			}
		}
		void radiusOut() {
			if(withinPOIradius) {
				withinPOIradius = false;
				interrupt();
			}
		}
		void close() {
			goOn = false;
			interrupt();
		}
		@Override
		public void run() {
			while(goOn) {
				synchronized(this) {
					try {
						wait();
					} catch (InterruptedException ignore) {
						if(!goOn) {
							break;
						}
					}
				}
				if(withinPOIradius) {
					useShortGPSDelay = true;
					// Wait 10 minutes;
					try {
						Thread.sleep(10*60*1000L);
					} catch (InterruptedException ignore) {
					}
					useShortGPSDelay = false;
				}
			}
		}
	}
	private class BackgroundSender extends Thread {
		WithinPOIRadiusSersor timer;
		private int locationId;
		private Location location;
		BackgroundSender() {
			timer = new WithinPOIRadiusSersor();
			timer.start();
		}
		void doIt(int locationId, Location location) {
			this.locationId = locationId;
			this.location = location;
			synchronized(this) {
				notifyAll();
			}
		}
		@Override
		public void run() {
			Looper.myLooper();
			Looper.prepare();
			while(true) {
				String host = Settings.getHost();
				try {
					synchronized(this) {
						wait();
					}

					// Check if you're within POI radius.
					if(isWithinRadius(location)) {
						// You're within POI radius.
						timer.radiusIn();
					} else {
						// You're out of POI radius.
						timer.radiusOut();
					}

					String username = Settings.getUsername();
					int port = Settings.getPort();
					int requestId = 1;
					String currentAddress = getArressesByLocation(location);
					double lat = location.getLatitude();
					double lon = location.getLongitude();
					String payload = String.format(TEMPLATE_REQUEST, requestId, username, locationId, currentAddress == null ? "": currentAddress, lat, lon);
					handleUIRequest("talkToServer","Tx " + payload);// Toast.makeText(PositionerService.this, "Tx " + payload, Toast.LENGTH_SHORT).show();
					//String response = "1;0.220;0507362270";//TODO
					String response = TalkToServer.talkToServer(host, port, payload, true);
					ServerResponse serverResponse = ServerResponse.parseServerResponse(response);
					boolean gpsAutoRefreshDistance = Settings.getGPSAutoRefreshDistance();
					if(gpsAutoRefreshDistance) {
						int gpsRefreshDistance = serverResponse.getGpsRefreshDistance();
						if(gpsRefreshDistance > 0) {
							int currentGPSRefreshDistance = Settings.getGPSRefreshDistance();
							if(currentGPSRefreshDistance != gpsRefreshDistance) {
								Settings.setGPSRefreshDistance(gpsRefreshDistance);
								// If within POI interval then gpsRefreshInterval is 10 seconds.
								// Otherwise, gpsRefreshInterval gets value from configuration.
								long gpsRefreshInterval = timer.useShortGPSDelay? 10*1000L: Settings.getGPSRefreshInterval() * 1000L;
//								removeLocationListener(locationListener);
								addLocationListener(locationListener, gpsRefreshDistance, gpsRefreshInterval);
							}
						}
					}
					handleUIRequest("talkToServer", "Rx " + response);// Toast.makeText(PositionerService.this, "Rx " + response, Toast.LENGTH_SHORT).show();
				} catch (UnknownHostException e) {
					handleUIRequest("talkToServer", "Send ERR: Unknown host " + host);// Toast.makeText(PositionerService.this, "Send ERR: Unknown host " + host, Toast.LENGTH_SHORT).show();
				} catch (IOException e) {
					handleUIRequest("talkToServer", "Send ERR: " + e.getMessage());// Toast.makeText(PositionerService.this, "Send ERR: " + e.getMessage(), Toast.LENGTH_SHORT).show();
				} catch (InterruptedException e) {
					handleUIRequest("talkToServer", "Interrupted. Quiting");
					break;
				} catch (Exception e) {
					handleUIRequest("talkToServer", "Send Global ERR: " + e.getMessage());// Toast.makeText(PositionerService.this, "Send ERR: " + e.getMessage(), Toast.LENGTH_SHORT).show();
				}
			}
		}
	};

	/*
	private final class UIHandler extends Handler {
		public static final int DISPLAY_UI_TOAST = 0;
		public static final int DISPLAY_UI_DIALOG = 1;

		public UIHandler(Looper looper) {
			super(looper);
		}

		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case UIHandler.DISPLAY_UI_TOAST: {
				Context context = getApplicationContext();
				Toast t = Toast.makeText(context, (String) msg.obj, Toast.LENGTH_LONG);
				t.show();
			}
			case UIHandler.DISPLAY_UI_DIALOG:
				// TBD
			default:
				break;
			}
		}
	}

	protected void handleUIRequest(String message) {
		Message msg = uiHandler.obtainMessage(UIHandler.DISPLAY_UI_TOAST);
		msg.obj = message;
		uiHandler.sendMessage(msg);
	}
*/
	protected void handleUIRequest(String tag, String message) {
		Log.d(tag, message);
		Intent intent = new Intent("custom-event-name");
		// You can also include some extra data.
		intent.putExtra("message", message);
		LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
	}
}